package org.apache.xinclude4flex.loaders {
	
	/**
	 * @author gavingui2011@gmail.com (Gavin Lei)
	 */
	
	import flash.events.ErrorEvent;
	import flash.events.Event;
	import flash.events.IOErrorEvent;
	import flash.events.ProgressEvent;
	import flash.events.SecurityErrorEvent;
	
	import org.apache.xinclude4flex.loaders.XMLLoader;
	import org.apache.xinclude4flex.events.MultiProgressEvent;
	
	import memorphic.xpath.XPathQuery;
	
	[Event(name = "progress", type = "madebypi.net.events.MultiProgressEvent")];
	public class XIncludeXMLLoader extends XMLLoader {
		
		private var _includeTable		:Object;
		private var _includesToLoad		:Array;
		private var _totalIncludes		:uint;
		private var _initialURL			:String;
		private var _baseURL			:String;
		
		public function XIncludeXMLLoader():void {
			super();
			_initialURL			= "";
			_includeTable 		= new Object();
			_includesToLoad		= new Array();
			_totalIncludes 		= 0;
		}
		
		/**
		 * Load an xml file
		 * @param	url		- url of an  xml file
		 * @param	forced  - if already loading, this will force the current connection to close and load the new url
		 */
		final override public function load(url:String, forced:Boolean = false):void {
			if (_initialURL == "") {
				_initialURL = url;
				_baseURL    = url.substring(0, url.lastIndexOf("/") + 1);
			}
			super.load(url, forced);
		}
		
		final override protected function onLoadComplete(e:Event):void {
			
			_loading 		= false;
			_loadComplete 	= true;
			
			var tXML:XML;
			if (_loader.data is String) {
				tXML = preParseForIncludes();
			}else{
				if (!hasEventListener(ErrorEvent.ERROR)) { throw new Error("Uncaught exception - Loaded data is not String, can not create XML"); }
				dispatchEvent(new ErrorEvent(ErrorEvent.ERROR, false, false, "Loaded data is not String, can not create XML"));
				return;
			}
			
			//update the main xml with the loaded xml
			updateXML(tXML);
			//check for xinclude nodes in loaded xml
			checkForIncludes();
			//see if there's more to load
			loadNextInclude();
		}
		
		/**
		 * Removes xmlns:xi namespace definition from the xml string and converts any xi:include to xinclude - makes things much easier to deal with in as3 if there are no namespaces in the xml
		 * Adds base url - the path of the main xml - to any include hrefs that are not absoulute paths
		 * @return
		 */
		private function preParseForIncludes():XML{
			var x	:XML;
			var x2	:XML;
			var s	:String = _loader.data;
			
			// remove any xmlns:xi definitions from the xml string
			s = s.replace(new RegExp("xmlns:xi=\"http://www.w3.org/2001/XInclude\"", "g"), "");
			// replace xi:include namespace nodes with <xinclude/> nodes
			s = s.replace(new RegExp("<xi:include ", "g"), "<xinclude ");
			// create the xml
			x = new XML(s);
			// add base url of the main xml file if the include href is a relative path
			var includeURL:String;
			for each(x2 in x..xinclude) {
				includeURL = x2.@href;
				if (includeURL.indexOf("://") == -1) { includeURL = _baseURL + includeURL; }
				x2.@href = includeURL;
			}
			
			return x;
		}
		
		final override protected function onLoadError(e:ErrorEvent):void {
			
			_loading 		= false;
			_loadComplete 	= true;
			_loadSuccess 	= false;
			
			var loadedURL:String = _request.url;
			
			if (_xml == null) {
				trace("Failed to load xml data from '"+loadedURL+"'");
				dispatchEvent(new ErrorEvent(e.type, false, false, e.text));
			}else{
				if (_includesToLoad.length > 0) {
					if (_includesToLoad[0] == loadedURL) {
						trace("Failed to load xinclude data from '"+loadedURL+"'");
						for each(var x:XML in _xml..xinclude.(@href == loadedURL)) {
							x.@loaderror = e.type;
						}
						_includesToLoad.shift();
						loadNextInclude();
					}
				}
			}
		}
		
		private function updateXML(x:XML):void {
			
			var loadedURL:String = _request.url;
			
			if (_xml == null){
				//stop self referencing
				_includeTable[loadedURL] = false;
				//set the main xml
				_xml = x;
			}else {
				if (_includesToLoad.length > 0) {
					if (_includesToLoad[0] == loadedURL){ //this should always be true for a loaded include...
						//clear the loaded item from the list
						_includesToLoad.shift();
						//add the loaded xinclude to the main xml
						while (_xml..xinclude.(@href == loadedURL).length() > 0) {
							//select the xpath query
							var q:String = _xml..xinclude.(@href == loadedURL)[0].attribute("xpointer");
							//check for a * query and  just add the loaded data
							if (q == "" || q == "*" || q == "//*" || q == "//") {
								_xml..xinclude.(@href == loadedURL)[0] = x;
							}else{
								//process the query and add the result to the main xml
								var xpathResult:* = new XPathQuery(q).exec(x);
								_xml..xinclude.(@href == loadedURL)[0] = xpathResult;
							}
						}
						//all references have been replaced so don't let this xml file be referenced again in another xinclude
						_includeTable[loadedURL] = false;
					}
				}
			}
		}
		
		private function checkForIncludes():void {
			
			var includeReferences:XMLList = _xml..xinclude;
			var n:int = includeReferences.length();
			var i:uint = 0;
			var x:XML;
			
			if (n > 0) {
				trace("checkForIncludes found " + n + " xinclude node(s)");
				while (i < n) {
					var includeURL	:String = includeReferences[i].@href;
					if (_includeTable[includeURL] == null){
						_includeTable[includeURL] = true;
						_includesToLoad.push(includeURL);
						_totalIncludes++;
					}else if(_includeTable[includeURL] == false){
						trace("Already registered xinclude @href="+includeURL+" in this xml - This will be ignored and marked as a cyclic reference");
						for each(x in _xml..xinclude.(@href == includeURL)){
							x.@loaderror = "cyclicReference";
						}
					}
					i++;
				}
			}else{
				trace("no (more) xinclude nodes found");
			}
		}

		private function loadNextInclude():void {
			if (_includesToLoad.length > 0) {
				this.load(_includesToLoad[0]);
			}else{
				_loadSuccess = true;
				trace("XIncludeXMLLoader - Loaded and parsed include(s)");
				dispatchEvent(new Event(Event.COMPLETE));
			}
		}
		
		override protected function onLoadProgress(e:ProgressEvent):void{
			
			var mpevent:MultiProgressEvent = new MultiProgressEvent(MultiProgressEvent.PROGRESS, false, false, e.bytesLoaded, e.bytesTotal);
			
			//event loadIndex and totalLoads are both -1 for the first xml load
			//subsequent loads are the xincludes loading. these events have those extra properties set
			
			if (_request.url != _initialURL) {
				mpevent.loadIndex  = _totalIncludes - _includesToLoad.length;
				mpevent.totalLoads = _totalIncludes;
			}
			dispatchEvent(mpevent);
		}
		
		public function get baseURL():String { return _baseURL; }
	}
}